package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"time"
)

const (
	pktHeadLen = 4
	zeroRev    = "0000000000000000000000000000000000000000"
)

var pktFlush = []byte("0000")

func die(msg ...string) {
	fmt.Fprintln(os.Stderr, msg)
	os.Exit(1)
}

type ref struct {
	Name   string
	OldRev string
	NewRev string
}

type Refs struct {
	R []*ref
}

func (r *Refs) AppedRef(line []byte) {
	arr := strings.SplitN(string(line), " ", 3)
	if len(arr) != 3 {
		return
	}
	r.R = append(r.R, &ref{
		Name:   arr[2],
		OldRev: arr[0],
		NewRev: arr[1],
	})
}

type pipe struct {
	reader io.Reader
	writer io.Writer
}

func NewPipe(in io.Reader, out io.Writer) *pipe {
	return &pipe{
		reader: in,
		writer: out,
	}
}

func (p *pipe) Read() ([]byte, error) {
	head := make([]byte, pktHeadLen)
	_, err := io.ReadFull(p.reader, head)
	if err != nil {
		return nil, err
	}
	lineLen, err := strconv.ParseInt(string(head), 16, 16)
	if err != nil {
		return nil, err
	}
	// flush
	if lineLen == 0 {
		return []byte("flush"), nil
	}
	line := make([]byte, lineLen-pktHeadLen)
	_, err = io.ReadFull(p.reader, line)
	if err != nil {
		return nil, err
	}
	return line, nil
}

func (p *pipe) Write(line []byte) error {
	if bytes.Equal(line, pktFlush) {
		return p.write(line)
	}
	pktLen := len(line)
	head := []byte(fmt.Sprintf("%04x", pktLen+pktHeadLen))

	pkt := append(head, line...)

	return p.write(pkt)
}

func (p *pipe) write(pkt []byte) error {
	_, err := p.writer.Write(pkt)
	return err
}

func buildPktHead(line []byte) []byte {
	len := len(line)
	if len == 0 {
		return pktFlush
	}
	head := []byte(fmt.Sprintf("%04x", len+pktHeadLen))
	return append(head, line...)
}

func main() {
	p := NewPipe(os.Stdin, os.Stdout)
	var line []byte
	var err error

	// Version and features negotiation.
	for {
		line, err = p.Read()
		if err != nil {
			die(err.Error())
		}
		fmt.Fprintln(os.Stderr, "[line]:", string(line))
		if bytes.Equal(line, []byte("flush")) {
			fmt.Fprintln(os.Stderr, "[find flush]!!")
			p.Write([]byte("version=1"))
			p.Write(pktFlush)
			break
		}
	}

	// Get all refs to be updated
	refs := &Refs{}
	for {
		line, err = p.Read()
		if err != nil {
			die(err.Error())
		}
		if bytes.Equal(line, []byte("flush")) {
			fmt.Fprintln(os.Stderr, "[find flush]!!")
			break
		}
		fmt.Fprintln(os.Stderr, "[line]:", string(line))
		refs.AppedRef(line)
	}

	// handle ref
	for _, v := range refs.R {
		fmt.Fprintln(os.Stderr, v.Name, "---", v.OldRev, "---", v.NewRev)

		// make receive-pack to update ref
		// p.Write([]byte("ok " + v.Name))
		// p.Write([]byte("option fall-through"))

		// reject update ref
		// p.Write([]byte("ng " + v.Name + " SURA will update it!!"))

		// update newrev to another ref
		t := time.Now().Unix()
		newRefName := "refs/heads/pr-auto-1-" + strconv.FormatInt(t, 10)
		args := []string{"update-ref", newRefName, v.NewRev}
		cmd := exec.Command("/usr/bin/git", args...)
		if err := cmd.Run(); err != nil {
			die("update ref err:", err.Error())
		}
		fmt.Fprintln(os.Stderr, "[update ref]", newRefName)

		// report update to another ref
		// The ref should be updated manually before then
		p.Write([]byte("ok " + v.Name))
		p.Write([]byte("option refname " + newRefName))
		p.Write([]byte("option old-oid " + zeroRev))
		p.Write([]byte("option new-oid " + v.NewRev))
		p.Write([]byte("option forced-update"))
	}

	p.Write(pktFlush)
}
